package com.imooc.bilibili.util;

import com.github.tobato.fastdfs.domain.fdfs.FileInfo;
import com.github.tobato.fastdfs.domain.fdfs.MetaData;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.AppendFileStorageClient;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.imooc.bilibili.exception.ConditionException;
import com.mysql.cj.util.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.util.*;

/**
 * FastDFS 工具类
 *
 * @author xiexu
 * @create 2022-06-17 18:26
 */
@Component
public class FastDFSUtil {

    @Autowired
    private FastFileStorageClient fastFileStorageClient;

    // 支持断点续传的依赖
    @Autowired
    private AppendFileStorageClient appendFileStorageClient;

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    // 默认是组1
    private static final String DEFAULT_GROUP = "group1";

    private static final String PATH_KEY = "path-key:";

    private static final String UPLOADED_SIZE_KEY = "uploaded-size-key:";

    private static final String UPLOADED_NO_KEY = "uploaded-no-key:";

    // 每个分片的大小
    private static final int SLICE_SIZE = 1024 * 1024 * 2; // 2M

    /**
     * 获取文件的类型
     *
     * @param file springboot封装的文件类
     * @return
     */
    public String getFileType(MultipartFile file) {
        if (file == null) {
            throw new ConditionException("非法文件！");
        }
        // 获取文件名称
        String fileName = file.getOriginalFilename();
        // 获取文件名称最后一个"."的位置，这样子可以根据"."的位置截取出文件的类型
        int index = fileName.lastIndexOf(".");
        // 获取文件的类型
        String fileType = fileName.substring(index + 1);
        return fileType;
    }

    /**
     * 上传一般文件
     *
     * @param file springboot封装的文件类
     * @return
     * @throws Exception
     */
    public String uploadCommonFile(MultipartFile file) throws Exception {
        Set<MetaData> metaDataSet = new HashSet<>();
        String fileType = this.getFileType(file);
        // 上传文件
        StorePath storePath = fastFileStorageClient.uploadFile(file.getInputStream(), file.getSize(), fileType, metaDataSet);
        // 返回文件在服务器的路径
        return storePath.getPath();
    }

    /**
     * 上传可以断点续传的文件
     *
     * @param file
     * @return
     * @throws IOException
     */
    public String uploadAppenderFile(MultipartFile file) throws Exception {
        // 获取文件名称
        String fileName = file.getOriginalFilename();
        // 获取文件类型
        String fileType = this.getFileType(file);
        /**
         * 上传可以断点续传的文件
         * 第一个参数：具体是哪一个组，一般默认是组1
         * 第二个参数：要上传的文件所对应的输入流
         * 第三个参数：文件大小
         * 第四个参数：文件类型
         */
        StorePath storePath = appendFileStorageClient.uploadAppenderFile(DEFAULT_GROUP, file.getInputStream(), file.getSize(), fileType);
        // 返回文件在服务器的路径
        return storePath.getPath();
    }

    /**
     * 修改可以断点续传的文件
     *
     * @param file     上传的文件
     * @param filePath 上一个分片的路径，或者整个断点续传文件的路径
     * @param offset   从文件的哪个位置开始添加分片
     */
    public void modifyAppenderFile(MultipartFile file, String filePath, long offset) throws Exception {
        /**
         * 第一个参数：组名
         * 第二个参数：文件路径
         * 第三个参数：上传文件的输入流
         * 第四个参数：文件的大小
         * 第五个参数：偏移量，从文件的哪个位置开始添加分片，也就是已经上传的文件分片的大小
         */
        appendFileStorageClient.modifyFile(DEFAULT_GROUP, filePath, file.getInputStream(), file.getSize(), offset);
    }

    /**
     * 根据文件路径删除文件
     *
     * @param filePath
     * @return
     */
    public void deleteFile(String filePath) {
        fastFileStorageClient.deleteFile(filePath);
    }

    // --------------------------- 演示文件分片的具体流程 ---------------------------

    /**
     * 通过文件分片来上传一个文件
     *
     * @param file         要上传的文件
     * @param fileMd5      文件经过MD5加密后，可以形成它唯一的标识符，可以进行秒传功能的开发，也可以用于redis的key
     * @param sliceNo      当前要上传的分片是第几片
     * @param totalSliceNo 总共要上传的分片数
     * @return 返回上传文件的路径
     */
    public String uploadFileBySlices(MultipartFile file, String fileMd5, Integer sliceNo, Integer totalSliceNo) throws Exception {
        if (file == null || sliceNo == null || totalSliceNo == null) {
            throw new ConditionException("参数异常！");
        }
        // -------------------- 跟redis相关的key -------------------------
        // 分片上传成功之后系统返回的文件路径
        String pathKey = PATH_KEY + fileMd5;
        // 当前已经上传成功的分片加起来的总大小
        String uploadedSizeKey = UPLOADED_SIZE_KEY + fileMd5;
        // 目前一共上传了多少个分片，和总分片数进行比对；如果相同说明已经完成了所有分片的上传
        String uploadedNoKey = UPLOADED_NO_KEY + fileMd5;
        // --------------------------------------------------------------

        // 先判断当前已经上传的文件分片的大小
        String uploadedSizeStr = redisTemplate.opsForValue().get(uploadedSizeKey);
        /**
         * 当前已经上传的文件分片的大小
         * 如果是第一个分片，那么分片大小此时应该是0
         * 如果不是第一个分片，需要获取当前文件传到多少了
         */
        Long uploadedSize = 0L;
        if (!StringUtils.isNullOrEmpty(uploadedSizeStr)) {
            uploadedSize = Long.valueOf(uploadedSizeStr);
        }

        // 获取文件类型
        String fileType = this.getFileType(file);
        // 如果上传的是第一个分片，用uploadAppenderFile方法
        if (sliceNo == 1) {
            // 第一个分片上传的文件路径，后续再上传分片的时候文件路径是不会改变的，因为我们后续用的是修改文件分片的方法
            String path = this.uploadAppenderFile(file);
            if (StringUtils.isNullOrEmpty(path)) {
                throw new ConditionException("上传失败！");
            }
            // 1.先把文件路径存储到redis的pathKey
            redisTemplate.opsForValue().set(pathKey, path);
            // 2.再把已经上传的文件大小更新到redis的uploadedSize
            uploadedSize += file.getSize();
            redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
            // 3.更新已经上传的分片数到redis的uploadedNoKey，这里是第一个分片所以直接传一个"1"就行
            redisTemplate.opsForValue().set(uploadedNoKey, "1");
        } else { // 如果要上传的不是第一个分片
            // 获取已经上传的文件分片的路径
            String filePath = redisTemplate.opsForValue().get(pathKey);
            if (StringUtils.isNullOrEmpty(filePath)) {
                throw new ConditionException("上传失败！");
            }
            // 上传的不是第一个分片，所以用modifyAppenderFile方法
            this.modifyAppenderFile(file, filePath, uploadedSize);
            // 1.更新已经上传的分片数+1
            redisTemplate.opsForValue().increment(uploadedNoKey);
            // 2.再把已经上传的文件大小更新到redis的uploadedSize
            uploadedSize += file.getSize();
            redisTemplate.opsForValue().set(uploadedSizeKey, String.valueOf(uploadedSize));
        }

        /**
         * 比对已经上传的文件分片数 和 总共要上传的分片数，如果一致说明文件上传完成，并且可以清除redis里面相关的key和value，
         * 然后返回给前端一个上传好的文件路径
         */
        String uploadedNoStr = redisTemplate.opsForValue().get(uploadedNoKey);
        // 已经上传成功的文件分片数
        Integer uploadedNo = Integer.valueOf(uploadedNoStr);
        // 上传的文件路径
        String resultPath = "";
        // 如果已经上传成功的文件分片数 == 总共要上传的文件分片数，就说明要结束整个上传了
        if (uploadedNo.equals(totalSliceNo)) {
            // 获取文件路径，用于返回给前端
            resultPath = redisTemplate.opsForValue().get(pathKey);
            // 保存redis相关的所有key
            List<String> keyList = Arrays.asList(pathKey, uploadedSizeKey, uploadedNoKey);
            // 清除redis相关的key和value
            redisTemplate.delete(keyList);
        }
        return resultPath;
    }

    /**
     * 把一个文件转换成多个分片
     *
     * @param multipartFile
     */
    public void convertFileToSlices(MultipartFile multipartFile) throws Exception {
        // 获取文件名称
        String fileName = multipartFile.getOriginalFilename();
        // 获取文件类型
        String fileType = this.getFileType(multipartFile);
        // 将multipartFile转化成java自带的文件类型
        File file = this.MultipartFileToFile(multipartFile);
        // 文件的大小
        long fileLength = file.length();
        // 计数器，方便后面文件分片名称的生成
        int count = 1;
        // i表示每个分片的起始位置
        for (int i = 0; i < fileLength; i += SLICE_SIZE) {
            // "r"表示读权限，读写权限就是"rw"，RandomAccessFile的特点就是可以跳到任意分片的起始位置
            RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r");
            // 定位到每个分片的起始点
            randomAccessFile.seek(i);
            byte[] bytes = new byte[SLICE_SIZE];
            // 读取每个分片的数据，返回的是读取到的分片大小
            int len = randomAccessFile.read(bytes);
            // 存储临时分片文件的路径，例如：1.mp4、2.mp4、3.mp4...
            String path = "/Users/xiexu/tmpfile/" + count + "." + fileType;
            File slice = new File(path);
            // 创建文件的输出流
            FileOutputStream fos = new FileOutputStream(slice);
            fos.write(bytes, 0, len);
            fos.close();
            randomAccessFile.close();
            count++;
        }
        // 删除临时文件
        file.delete();
    }

    /**
     * 将MultipartFile转换成File类型
     *
     * @param multipartFile
     * @return
     */
    public File MultipartFileToFile(MultipartFile multipartFile) throws Exception {
        // 获取文件名称
        String originalFilename = multipartFile.getOriginalFilename();
        // 数组包含文件名称和文件类型，例如：mianshi.pdf
        String[] fileName = originalFilename.split("\\.");
        File file = File.createTempFile(fileName[0], "." + fileName[1]);
        multipartFile.transferTo(file);
        return file;
    }

    @Value("${fdfs.http.storage-addr}")
    private String httpFdfsStorageAddr;

    /**
     * @param request
     * @param response
     * @param path     文件路径
     */
    public void viewVideoOnlineBySlices(HttpServletRequest request, HttpServletResponse response, String path) throws Exception {
        // 获取文件的信息
        FileInfo fileInfo = fastFileStorageClient.queryFileInfo(DEFAULT_GROUP, path);
        // 获取文件的总大小
        long totalFleSize = fileInfo.getFileSize();
        // http://1.12.65.61:8888/group1/M00/00/00/AQxBPWKsnCyECfnGAAAAAF8P79Y271.mp4
        // 文件实际所在的路径(文件服务器的路径)
        String url = httpFdfsStorageAddr + path;
        // 获取前端传过来的所有请求头
        Enumeration<String> headerNames = request.getHeaderNames();
        Map<String, Object> headers = new HashMap<>();
        // 遍历headerNames
        while (headerNames.hasMoreElements()) {
            String header = headerNames.nextElement();
            headers.put(header, request.getHeader(header));
        }
        // 获取前端传过来的Range参数
        String rangeStr = request.getHeader("Range");
        String[] range;
        if (StringUtils.isNullOrEmpty(rangeStr)) {
            rangeStr = "bytes=0-" + (totalFleSize - 1);
        }

        // ------------------ Request Headers的Range参数 -------------------------------

        // Range: bytes=9240576-36238125
        // 切割成：bytes、9240576、36238125 三个参数
        range = rangeStr.split("bytes= | -");
        // Range的起始位置
        long begin = 0;
        // 长度大于等于2，表示只有两个参数，一个是空字符串，另一个是起始位置
        if (range.length >= 2) {
            // 获取文件的起始位置
            begin = Long.parseLong(range[1]);
        }
        // Range的终止位置
        long end = totalFleSize - 1;
        if (range.length >= 3) {
            // 获取文件的结束位置
            end = Long.parseLong(range[2]);
        }
        // 实际的分片长度
        long len = (end - begin) + 1;

        // ------------------ Response Headers的Content-Range参数 -------------------------------

        // Content-Range: bytes 9240576-36238125/36238126
        String contentRange = "bytes " + begin + "-" + end + "/" + totalFleSize;
        response.setHeader("Content-Range", contentRange);
        response.setHeader("Accept-Ranges", "bytes");
        response.setHeader("Content-Type", "video/mp4");
        response.setContentLength((int) len);
        response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
        HttpUtil.get(url, headers, response);
    }
}
